using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Security.Permissions;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using Fuzzware.Common;
using Fuzzware.Common.XML;
using Fuzzware.ConvertFromXML.Processors;

namespace Fuzzware.ConvertFromXML
{
    public class OutputWriter : XmlWriter
    {
        XPathNavigator XPathNav;
        MemoryStream OutputStream;
        OutputSettings oOutputSettings;
        WriteState writeState;

        OutputNode RootNode;
        List<OutputNode> OutputNodes;
        List<String> PIforNextNode;

        public OutputWriter(MemoryStream stream, OutputSettings oOutputSettings)
        {
            OutputNodes = new List<OutputNode>();
            OutputStream = stream;
            this.oOutputSettings = oOutputSettings;
            writeState = WriteState.Start;
            byte[] Preamble = Settings.Encoding.GetPreamble();
            stream.Write(Preamble, 0, Preamble.Length);

            PIforNextNode = new List<String>();
        }

        public override XmlWriterSettings Settings
        {
            get
            {
                return oOutputSettings.XmlSettings;
            }
        }

        private void WriteToOutput(String str)
        {
            // Encode the string appropriately
            byte[] output = Settings.Encoding.GetBytes(str);
            // Write to stream
            OutputStream.Write(output, 0, output.Length);
        }

        public override void WriteStartElement(string prefix, string localName, string ns)
        {
            writeState = WriteState.Element;

            // Create new output node
            OutputNode Node = null;
            
            // Add it as a child of the last node in the hierarchy
            if (0 == OutputNodes.Count)
            {
                // Get an XPathNav to the root node
                XPathNav = XMLHelper.GetRootNode(oOutputSettings.XMLDoc);
                // Create the OutputNode
                Node = new OutputNode(oOutputSettings, null, PIforNextNode, XPathNav.Clone());
                PIforNextNode.Clear();
                // Remember the root node
                RootNode = Node;
            }
            else
            {
                // Set the XPathNav to the node we are writing.  If we are not on the node already, move to it.
                if(!(XPathNav.LocalName.Equals(localName) && XPathNav.NamespaceURI.Equals(ns) && XPathNav.Prefix.Equals(prefix)))
                    if (!XPathNav.MoveToFollowing(localName, ns))
                        Log.Write(MethodBase.GetCurrentMethod(), "Failed to move XPathNavigator object to named node", Log.LogType.Warning);

                Node = new OutputNode(oOutputSettings, OutputNodes[OutputNodes.Count - 1], PIforNextNode, XPathNav.Clone());
                PIforNextNode.Clear();
                OutputNodes[OutputNodes.Count - 1].AddChild(Node);
            }

            // Add it to the hierarchy
            OutputNodes.Add(Node);

            // Get the current node to write its start element
            Node.WriteStartElement(prefix, localName, ns);
        }

        public override void WriteFullEndElement()
        {
            // Get the current node to write its full end element
            OutputNodes[OutputNodes.Count - 1].WriteFullEndElement();

            // Remove it from the hierarchy
            OutputNodes.RemoveAt(OutputNodes.Count - 1);
        }

        public override void WriteStartAttribute(string prefix, string localName, string ns)
        {
            writeState = WriteState.Attribute;

            // Get the current node to write the start of an attribute
            OutputNodes[OutputNodes.Count - 1].WriteStartAttribute(prefix, localName, ns);
        }

        public override void WriteEndAttribute()
        {
            // Get the current node to write the end of an attribute
            OutputNodes[OutputNodes.Count - 1].WriteEndAttribute();

            writeState = WriteState.Element;
        }

        public override void WriteProcessingInstruction(string name, string text)
        {
            // Only write out processing instructions not meant for this app            
            if (name == "xml")
            {
                if (!Settings.OmitXmlDeclaration)
                {
                    // Change the encoding to be the actual encoding being used
                    int offset = text.IndexOf("encoding=\"");
                    if (offset == -1)
                    {
                        // No encoding specified so add it onto the end
                        text += (" encoding=\"" + Settings.Encoding.WebName + "\"");
                    }
                    else
                    {
                        // Insert the desired encoding into the PI
                        int rest = text.IndexOf("\"", offset + 10);
                        text = text.Substring(0, offset + 10) + Settings.Encoding.WebName + text.Substring(rest, text.Length - rest);
                    }
                }
                else
                    return;
            }
            else if ((name == XMLHelper.AppName()) || (name == "Schemer"))
            {
                // Get the current node to remember a PI for this application
                PIforNextNode.Add(text);
                // We never print out PI's for this application
                return;
            }

            // Write out the PI
            if (0 == OutputNodes.Count)
            {
                WriteToOutput("<?" + name + " " + text + "?>");
                if (oOutputSettings.XmlSettings.Indent)
                    WriteToOutput(oOutputSettings.XmlSettings.NewLineChars);
                return;
            }
            else
            {
                OutputNodes[OutputNodes.Count - 1].WriteProcInst(name + " " + text);
            }
        }



        public override void Close()
        {
            OutputStream.Close();
        }

        public override void Flush()
        {
            // Process the XML nodes, this will update any XML node values that are functions of other XML nodes
            NodePostProcessor oNodePostProcessor = new NodePostProcessor();
            oNodePostProcessor.Process(RootNode);

            // Serialise the output nodes from the root node
            RootNode.SerialiseAndClose(OutputStream);
            OutputStream.Flush();
            // Clean-up
            RootNode.Dispose();
        }

        public override string LookupPrefix(string ns)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteBase64(byte[] buffer, int index, int count)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteCData(string text)
        {
            if (writeState == WriteState.Element)
            {
                OutputNodes[OutputNodes.Count - 1].WriteCData(text);
                return;
            }
            Log.Write(MethodBase.GetCurrentMethod(), "OutputWriter encountered CDATA in an unexpected place", Log.LogType.Error);
        }

        public override void WriteCharEntity(char ch)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteChars(char[] buffer, int index, int count)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteComment(string text)
        {
            // If no nodes exist yet, write the comment straight out
            if ((0 == OutputNodes.Count) && !oOutputSettings.Config.OmitComments)
            {
                WriteToOutput("<!--" + text + "-->");
                if (oOutputSettings.XmlSettings.Indent)
                    WriteToOutput(oOutputSettings.XmlSettings.NewLineChars);
                return;
            }

            // Comments are a problem, we may want to keep them e.g. script in HTML, but if we ever process a XML block
            // we need to make sure they aren't included in that.  Comments can probably be handled mush better by adding them
            // as their own node, appropriately encoded, with XML markup removed.

            // We will support comments in between elements, but not elsewhere, in attributes for example
            if (writeState == WriteState.Element)
            {
                OutputNodes[OutputNodes.Count - 1].WriteComment(text);
            }
        }

        public override void WriteDocType(string name, string pubid, string sysid, string subset)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteEndDocument()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteEndElement()
        {
            // Get the current node to write its end element
            OutputNodes[OutputNodes.Count - 1].WriteEndElement();

            // Remove it from the hierarchy
            OutputNodes.RemoveAt(OutputNodes.Count - 1);
        }

        public override void WriteEntityRef(string name)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteRaw(string data)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteRaw(char[] buffer, int index, int count)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteStartDocument(bool standalone)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteStartDocument()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override WriteState WriteState
        {
            get { return writeState; }
        }

        private bool ContainsIllegalCharacters(String text)
        {
            if (-1 != text.IndexOf('<'))
                return true;
            if (-1 != text.IndexOf('&'))
                return true;
            // Only the characters "<" and "&" are strictly illegal in XML, however the characters below also have character entities.
            //if (-1 != text.IndexOf('>'))
            //    return true;
            //if (-1 != text.IndexOf('\''))
            //    return true;
            //if (-1 != text.IndexOf('\"'))
            //    return true;
            return false;
        }

        private String ReplaceIllegalCharacters(String text)
        {
            text = text.Replace("<", "&lt;");
            text = text.Replace(">", "&gt;");
            text = text.Replace("&", "&amp;");
            text = text.Replace("\"", "&quot;");
            text = text.Replace("'", "&apos;");
            return text;
        }

        public override void WriteString(string text)
        {
            if (WriteState == WriteState.Element)
            {
                // Check if the element text contains any illegal XML characters, if so put in a CDATA block
                if (ContainsIllegalCharacters(text))
                {
                    WriteCData(text);
                }
                else
                {
                    // Get the current node to write element value
                    OutputNodes[OutputNodes.Count - 1].WriteElementValue(text);
                }
                return;
            }
            else if (WriteState == WriteState.Attribute)
            {
                text = ReplaceIllegalCharacters(text);
                // Get the current node to write attribute value
                OutputNodes[OutputNodes.Count - 1].WriteAttributeValue(text);
                return;
            }

            Log.Write(MethodBase.GetCurrentMethod(), "OutputWriter in an invalid state", Log.LogType.Error);
        }

        public override void WriteSurrogateCharEntity(char lowChar, char highChar)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void WriteWhitespace(string ws)
        {
            throw new Exception("The method or operation is not implemented.");
        }
    }
}
